/**
 * The base windowing class.
 * Window is a wrapper for a particular DOM-element which
 * can receive various document events and respond to that events directly.
 * In compliance with most browsers' events model, the event bubbles up
 * to the parent windows. Also the window can be attached to another DOM-element
 * to handle its events too.
 *
 * @author Andrey Anisimov <hercules@inbox.ru>
 */

import core.ui.Event;
import core.util.Listeners;

class Window {

	/**
	 * Creates new window.
	 *
	 * Note:
	 * If parent is specified, then the window creates within parent window
	 * and adds itself to parent's children collection.
	 *
	 * If no element is specified, then window creates block element (div by default)
	 * and attaches to it. Otherwise the window attaches to the specified object.
	 *
	 * @param parent (Window) [optional] parent window
	 * @param x, y (int) [optional] window coordinates
	 * @param width, height (int) [optional] window size
	 * @param element (HTMLElement) [optional] existing dom element.
	 */
	this.Window = function(parent, x, y, width, height, element) {

		this.visible = false;
		this.enabled = true;
		this.children = {};
        this.id = this.getClass().getSimpleName() + Window.NEXTID++;
		this.parent = parent || null;

		if (!element) {

			// Create new div element
			element = this.createElement();
			element.className = this.getClass().getSimpleName();
			element.style.overflow = 'hidden';

			// Append to parent object
			if (parent instanceof Window) {
				element.style.position = 'relative';
				parent.addElement(element);
			} else {
				element.style.position = 'absolute';
				element.style.visibility = 'hidden';
				element.style.zIndex = Window.NEXTZINDEX++;
				document.body.appendChild(element);
			}
		}

		this.attach(element);	// Attach to HTML element

		// Add new window to parent's child collection
		if (parent) parent.children[this.getId()] = this;

		if (x)			element.style.left = ('string' == typeof x) ? x : (x + 'px');
		if (y)			element.style.top = ('string' == typeof y) ? y : (y + 'px');
		if (width)		element.style.width = ('string' == typeof width) ? width : (width + 'px');
		if (height)	element.style.height = ('string' == typeof height) ? height : (height + 'px');

		// Add new window to all windows collection
		Window.all[this.getId()] = Window.active = this;
	}

	/**
	 * Attaches window to DOM node to handle all its events.
	 *
	 * @param element (HTMLElement) element to attach to
	 */
	this.attach = function(node) {
		node.winid = this.getId();
		this.eventHandlerMethod = bind(this, this.handleEvent);
		for (var i = 0; i < Event.types.length; i++) {
			Event.addListener(node, Event.types[i], this.eventHandlerMethod);
		}
		if (!this.element) {
			this.element = node;
		}
	}

	/**
	 * Detaches window from node and restores previous event handlers.
	 *
	 * @param element (HTMLElement) [optional] element to detach from (this.element by default)
	 */
	this.detach = function(node) {
		if (!node) {
			node = this.element;
		}
		if (this.eventHandlerMethod) {
			for (var i = 0; i < Event.types.length; i++) {
				Event.removeListener(node, Event.types[i], this.eventHandlerMethod);
			}
		}
		node.winid = undefined;
		if (this.element == node) {
			delete this.element;
			this.element = undefined;
		}
	}

	/**
	 * Determines whether the window is currently attached.
	 *
	 * @return bool
	 */
	this.isAttached = function() {
		return (this.element !== undefined);
	}

	/**
	 * Attaches window to global document events.
	 * Note: you can use global events, in example, to intercept
	 * mouse events and implement proper behaviour.
	 *
	 * @param eventType (string) event type name (without 'on' prefix).
	 *
	 * @example
	 *
	 * <code>// A popup menu which hides when the user clicks in the outside area.
	 * var wnd = new Window();
	 * wnd.attachGlobalEvent('mousedown');
	 * wnd.onmousedown = function(event, target, current) {
	 *   	if (this != target) {
	 *			// Hide the window...
	 *  	} else {
	 *  		// Implement another logic...
	 *  	}
	 * }
	 * </code>
	 */
	this.attachGlobalEvent = function(eventType) {
		if (!Window.global[eventType]) Window.global[eventType] = {};
		Window.global[eventType][this.getId()] = true;
	}

	/**
	 * Detaches global event.
	 *
	 * @param eventType (string) event type name (without 'on' prefix).
	 */
	this.detachGlobalEvent = function(eventType) {
		if (Window.global[eventType]) {
			delete Window.global[eventType][this.getId()];
		}
	}

	/**
	 * Returns attached DOM-element.
	 *
	 * @return HTMLElement
	 */
	this.getElement = function() {
		return this.element;
	}

	/**
	 * Returns parent window.
	 *
	 * @return Window
	 */
	this.getParent = function() {
		return this.parent;
	}

	/**
	 * Sets this window visible or hidden.
	 *
	 * @param show (bool) true to show, false to hide.
	 */
	this.setVisible = function(show) {
		this.visible = show;
		if (show) {
			this.setStyle('visibility', (this.isChild()) ? 'inherit' : 'visible');
		} else {
			this.setStyle('visibility', 'hidden');
		}
	}

	/**
	 * Makes this window visible.
	 * 
	 * @see setVisible(true);
	 */
	this.show = function() {
		this.setVisible(true);
	}

	/**
	 * Makes this window hidden.
	 *
	 * @see setVisible(false);
	 */
	this.hide = function() {
		this.setVisible(false);
	}

	/**
	 * Determines whether this window is hidden.
	 *
	 * @return bool
	 */
	this.isHidden = function() {
		return ('hidden' == this.getStyle('visibility'));
	}

	/**
	 * Determines whether this window is visible.
	 *
	 * @return bool
	 */
	this.isVisible = function() {
		return !this.isHidden();
	}

	/**
	 * Moves window to the specified coordinates relatievly parent left top.
	 * If this is a top window, then moves relatievly document body left and top corner.
	 *
	 * @param x, y (int) new window coordinates.
	 */
	this.move = function(x, y) {
		this.element.style.left = x + 'px';
		this.element.style.top = y + 'px';
	}

	/**
	 * Resizes window to the specified width and height.
	 *
	 * @param width, height (int) new window size.
	 */
	this.resize = function(width, height) {
		this.element.style.width = width + 'px';
		this.element.style.height = height + 'px';
	}

	/**
	 * Centeres the window within its parent.
	 *
	 * Note: centering has different effect in various browsers
	 * if it used in strict mode!
	 */
	this.center = function() {
		var x, y;
		if (this.isChild()) {
			x = this.parent.getClientWidth()/2 - this.getWidth()/2 + this.parent.getElement().scrollLeft/2;
			y = this.parent.getClientHeight()/2 - this.getHeight()/2 + this.parent.getElement().scrollTop/2;
		} else {
			x = Window.getWidth()/2 - this.getWidth()/2 + document.body.scrollLeft/2;
			y = Window.getHeight()/2 - this.getHeight()/2 + document.body.scrollTop/2;
		}
		this.move(Math.round(x), Math.round(y));
	}

	/**
	 * Returns window identificator.
	 *
	 * @return string
	 */
	this.getId = function() {
		return this.id;
	}

	/**
	 * Sets the inner text for window.
	 *
	 * Note: this will remove all previous window content!
	 *
	 * @param text (string) window text.
	 */
	this.setText = function(text) {
		this.element.innerHTML = text;
	}

	/**
	 * Returns window text.
	 *
	 * @return string
	 */
	this.getText = function() {
		return this.element.innerHTML;
	}

	/**
	 * Returns left window coordinate relatievly parent's left in pixels.
	 *
	 * @return int
	 */
	this.getX = function() {
		return this.element.offsetLeft;
	}

	/**
	 * Returns top window coordinate relatievly parent's top in pixels.
	 *
	 * @return int
	 */
	this.getY = function() {
		return this.element.offsetTop;
	}

	/**
	 * Retrieves absolute x coordinate in pixels.
	 *
	 * @return int
	 */
	this.getAbsX = function() {
		var element = this.getElement();
		var x = element.offsetLeft;
		while (element = element.offsetParent) {
			x += element.offsetLeft;
			x -= element.scrollLeft;
		}
		return document.body.scrollLeft + x;
	}

	/**
	 * Retrieves absolute y coordinate in pixels.
	 *
	 * @return int
	 */
	this.getAbsY = function() {
		var element = this.getElement();
		var y = element.offsetTop;
		while (element = element.offsetParent) {
			y += element.offsetTop;
			y -= element.scrollTop;
		}
		return document.body.scrollTop + y;
	}

	/**
	 * Retrieves window width in pixels.
	 *
	 * @return int
	 */
	this.getWidth = function() {
		return this.element.offsetWidth;
	}

	/**
	 * Retrieves window height in pixels.
	 *
	 * @return int
	 */
	this.getHeight = function() {
		return this.element.offsetHeight;
	}

	/**
	 * Retrieves client area width in pixels.
	 *
	 * @return int
	 */
	this.getClientWidth = function() {
		return this.element.clientWidth;
	}

	/**
	 * Retrieves client area height in pixels.
	 *
	 * @return int
	 */
	this.getClientHeight = function() {
		return this.element.clientHeight;
	}

	/**
	 * Retrieves client area left coordinate.
	 *
	 * @return int
	 */
	this.getClientX = function() {
		// TODO: return this.element.clientLeft;
		// @see https://bugzilla.mozilla.org/show_bug.cgi?id=111207
		return (this.getWidth() - this.getClientWidth())/2;
	}

	/**
	 * Retrieves client area top coordinate.
	 *
	 * @return int
	 */
	this.getClientY = function() {
		// TODO: return this.element.clientTop (see https://bugzilla.mozilla.org/show_bug.cgi?id=111207)
		return (this.getHeight() - this.getClientHeight())/2;
	}

	/**
	 * Sets the window tool tip.
	 *
	 * @param text (string) text for the tip.
	 */
	this.setToolTip = function(text) {
		this.element.title = text;
	}

	/**
	 * Retrieves the window tool tip text.
	 *
	 * @return string
	 */
	this.getToolTip = function() {
		return this.element.title;
	}

	/**
	 * Closes the window and its child windows.
	 */
	this.close = function() {
		for (var id in this.children) {
			this.children[id].close();
			delete this.children[id];
		}
		if (this.parent) {
			delete this.parent.children[this.getId()];
			delete this.parent;
		}
		if (this.element) {
			if (this.element.parentNode) this.element.parentNode.removeChild(this.element);
			delete this.element;
		}
		delete Window.all[this.getId()];
	}

	/**
	 * Determines whether this window is closed.
	 *
	 * @return bool
	 */
	this.isClosed = function() {
		return (!Window.all[this.getId()]);
	}

	/**
	 * Sets this window active.
	 * For top windows it means that the window obtains the higher z-index.
	 * For child (non-top) windows they just makes their parent active.
	 */
	this.activate = function() {
		if (!this.isActive()) {
			if (this.isChild()) {
				this.parent.activate();
			} else {
				// Increase z-index for parent windows
				this.setStyle('z-index', Window.NEXTZINDEX++);
				Window.active = this;
			}
		}
	}

	/**
	 * Gets whether the window is currently active.
	 *
	 * @return bool
	 */
	this.isActive = function() {
		return (Window.active && Window.active == this);
	}

	/**
	 * Sets the window enabled or disabled.
	 *
	 * @param enabled (bool) true to enable, false to disable.
	 */
	this.setEnabled = function(enabled) {
		this.enabled = enabled;
		this.getElement().disabled = !enabled;
		for (var id in this.children) {
			this.children[id].setEnabled(enabled);
		}
	}

	/**
	 * Enables the window.
	 * @see setEnabled(true).
	 */
	this.enable = function() {
		this.setEnabled(true);
	}

	/**
	 * Disables the window.
	 * @see setEnabled(false).
	 */
	this.disable = function() {
		this.setEnabled(false);
	}

	/**
	 * Gets whether this window is currently enabled.
	 *
	 * @return bool
	 */
	this.isEnabled = function() {
		return this.enabled;
	}

	/**
	 * Gets whether this window is currently disabled.
	 *
	 * @return bool
	 */
	this.isDisabled = function() {
		return this.getElement().disabled;
	}

	/**
	 * Gets whether this window is a child window.
	 *
	 * @return bool
	 */
	this.isChild = function() {
		return (null != this.parent);
	}

	/**
	 * Gets whether this window is a child of specified window.
	 *
	 * @param window (Window) parent window.
	 * @return bool
	 */
	this.isChildOf = function(window) {
		for (var id in window.children) {
			if (window.children[id] == this) return true;
			if (this.isChildOf(window.children[id])) return true;
		}
		return false;
	}

    this.setOpacity = function(t) {
        this.setStyle('opacity', t);
    }

    this.getOpacity = function () {
        return this.getStyle('opacity');
    }

    this.setTransparency = function(t) {
        this.setStyle('opacity', 100 - t);
    }

    this.getTransparency = function () {
        return 100 - this.getStyle('opacity');
    }

	/**
	 * Window styling functions.
	 * You can style your GUI elements simply just calling these functions and/or include the CSS-file.
	 * See the description below about how the CSS rules will be applied to your GUI elements.
	 */

	/**
	 * Sets style parameter.
	 *
	 * Note: you can use either camelCased or hyphenated names for CSS properties.
	 *
	 * @param name (string) CSS property name.
	 * @param value (string) property value.
	 */
	this.setStyle = function(name, value) {
		switch (name.toLowerCase()) {
			case 'float':
				this.element.style['styleFloat'] = this.element.style['cssFloat'] = value;
				break;
			case 'opacity':
				this.element.style.opacity = value/100;			// CSS3 opacity
				this.element.style.MozOpacity = value/100;		// Mozilla case
				this.element.style.KhtmlOpacity = value/100;	// Konqueror
				this.element.style.filter = 'progid:DXImageTransform.Microsoft.Alpha(opacity=' + value + ')';	// IE6
				break;
			default:
				var camelName = name.replace(/\-([a-z])/g, function(m) { return m.charAt(1).toUpperCase(); });
				this.element.style[camelName] = value;
		}
	}

	/**
	 * Returns style value by name.
	 *
	 * Note: name can be either camelCased or hyphenated.
	 * By calling this method you can access only to inline style values.
	 * External style properties which are defined, for example, in CSS-file, aren't accessible.
	 *
	 * @param name (string) CSS property name.
	 * @return string
	 */
	this.getStyle = function(name) {
		switch (name.toLowerCase()) {
			case 'float':
				return this.element.style['styleFloat']
					? this.element.style['styleFloat']
					: this.element.style['cssFloat'];
			case 'opacity':
				return (this.element.style.opacity || (0 === this.element.style.opacity))
					? (this.element.style.opacity*100)
					: 100;
			default:
				var camelName = name.replace(/\-([a-z])/g, function(m) { return m.charAt(1).toUpperCase(); });
				return this.element.style[camelName];
		}
	}

	/**
	 * Sets the id of element so you can use it as a selector for particular GUI-element
	 * for which wasn't created a particular class.
	 *
	 * @param id (string) new id.
	 *
	 * @example
	 *
	 * <code lang="javascript">
	 * var static = new Static();
	 * static.setStyleId('static-custom');
	 * ...
	 * </code>
	 *
	 * <code lang="css">
	 * #static-custom {
	 * 		background-color: #ff0000;
	 * }
	 * </code>
	 */
	this.setStyleId = function(id) {
		this.element.id = id;
	}

	/**
	 * Returns the element id.
	 *
	 * @return string
	 */
	this.getStyleId = function() {
		return this.element.id;
	}

	/**
	 * Sets the class name for GUI elements.
	 * By default the class name is a JavaScript class name (w/o package path).
	 *
	 * @param name (string) new name.
	 *
	 * @example
	 *
	 * <code lang="javascript">
	 * var button = new Button();
	 * button.setStyleName('CustomButton');
	 * </code>
	 *
	 * <code lang="css">
	 * .CustomButton {
	 *		border: 1px solid red;
	 * }
	 * </code>
	 */
	this.setStyleName = function(name) {
		this.element.className = name;
	}

	/**
	 * Retrieves current style name.
	 *
	 * @return string
	 */
	this.getStyleName = function() {
		return this.element.className.split(' ')[0];
	}

	/**
	 * Adds style name to the window.
	 *
	 * Note: one window can have multiple style names,
	 * accordingly multiple style rules in the CSS.
	 *
	 * @param name (string) additional name.
	 *
	 * @example
	 *
	 * <code lang="javascript">
	 *
	 * // Both CSS rules will be applied to the wnd object.
	 * var wnd = new Window();
	 * wnd.addStyleName('SpecialWindow');
	 * 
	 * </code>
	 *
	 * <code lang="css">
	 * .Window {
	 *  	background-color: lightblue;
	 *   	border: 1px solid blue;
	 * }
	 * .SpecialWindow {
	 *  	border: 1px solid red;
	 * }
	 * </code>
	 */
	this.addStyleName = function(name) {
		var names = this.element.className.split(' ');
		for (var i = 0; i < names.length; i++) {
			if (name == names[i]) return;
		}
		names.push(name);
		this.element.className = names.join(' ');
	}

	/**
	 * Removes style name from the window.
	 *
	 * @param name (string) name to remove
	 */
	this.removeStyleName = function(name) {
		var names = this.element.className.split(' ');
		for (var i = 0; i < names.length; i++) {
			if (name == names[i]) {
				names.splice(i, 1);
				break;
			}
		}
		this.element.className = names.join(' ');
	}

	/**
	 * Clears all style names except the first (main) style name.
	 */
	this.clearStyleName = function() {
		var names = this.element.className.split(' ');
		this.element.className = names[0] || this.constructor.className;
	}

	/**
	 * @protected
	 * Creates and returns root HTML element for this window.
	 *
	 * Note: you can override or replace this method if you want
	 * to create another element than div as root element for the window.
	 *
	 * @return HTMLElement
	 */
	this.createElement = function() {
		return document.createElement('div');
	}

	/**
	 * @protected
	 * Adds new child element to the window.
	 *
	 * Note: override this method if you want to use
	 * custom container for all child windows.
	 *
	 * @param child (HTMLElement) child element.
	 */
	this.addElement = function(child) {
		this.getElement().appendChild(child);
	}

	/**
	 * @protected
	 * Common event handler function.
	 * Calls when event occurs in attached element.
	 *
	 * @param event (object) [optional] browser-specific event object.
	 * @return bool
	 */
	this.handleEvent = function(event) {
		event = Event.improve(event);
		var element = event.getTarget();
		var target = (element) ? Window.all[element.winid] : undefined;
		var returnValue = Window.processEvent(event, target);
		if (!event.isStopped()) {
			if (('function' == typeof this['on' + event.getType()]) && this.isEnabled()) {
				if (false === this['on' + event.getType()](event, target, this)) {
					returnValue = false;
				}
			}
		}
		if (false === returnValue) {
			event.cancel();
		}
		return returnValue;
	}
}

/**
 * @private
 * Global document's events handler.
 *
 * @param event (object) [optional] browser-specific event object.
 * @return bool
 */
Window.eventHandler = function(event) {
	event = Event.improve(event);
	var element = event.getTarget();
	var target = (element) ? Window.all[element.winid] : undefined;
	if (false === Window.processEvent(event, target)) {
		event.cancel();
	}
	return !event.isCancelled();
}

/**
 * @private
 * Processes event by all global attached windows.
 *
 * @param event (Event) current event
 * @param target (Window) target window
 * @return bool
 */
Window.processEvent = function(event, target) {
	var returnValue = true;
	for (var id in Window.global[event.getType()]) {
		var current = Window.all[id];
		if (('function' == typeof current['on' + event.getType()]) && current.isEnabled()) {
			if (false === current['on' + event.getType()](event, target, current)) {
				returnValue = false;
			}
		}
	}
	return returnValue;
}

/**
 * Window id counter.
 * @type int
 */
Window.NEXTID = 1;

/**
 * Z-index counter.
 * @type int
 */
Window.NEXTZINDEX = 1;

/**
 * Windows collection (keys are window ids).
 * @type hash table
 */
Window.all = {};

/**
 * Current active window.
 * @type Window
 */
Window.active = null;

/**
 * Collection of global attached windows.
 * Keys are event types, and values are hash tables of Window.
 * @type hash table
 */
Window.global = {};

/**
 * Returns current browser window width.
 *
 * @return int
 */
Window.getWidth = function() {
	// TODO: implement DOCTYPE-safety logic.
	return document.body.clientWidth +
		document.body.scrollLeft;
}

/**
 * Returns current browser window height.
 *
 * @return int
 */
Window.getHeight = function() {
	// TODO: implement DOCTYPE-safety logic.
	return document.body.clientHeight +
		document.body.scrollTop;
}

/**
 * Returns the window by its id.
 *
 * @param id (string) The id of the window.
 * @return Window
 */
Window.forId = function(id) {
	return Window.all[id];
}

/**
 * Sets default document event handlers to Window.eventHandler.
 *
 * @param doc (HTMLDocument) source document.
 */
Window.registerDocument = function(doc) {
	for (var i = 0; i < Event.types.length; i++) {
		Event.addListener(doc, Event.types[i], Window.eventHandler);
	}
}

/**
 * Unregisters document from the event handling.
 *
 * @param doc (HTMLDocument) document to unregister.
 */
Window.unregisterDocument = function(doc) {
	for (var i = 0; i < Event.types.length; i++) {
		Event.removeListener(doc, Event.types[i], Window.eventHandler);
	}
}

/**
 * Closes all created windows.
 * Note: this function is used to force memory clean in some browsers.
 */
Window.cleanup = function() {
	for (var id in Window.all) {
		if (!Window.all[id].isChild()) {
			Window.all[id].close();
		}
	}
}

Window.listeners = new Listeners([
    'onWindowResized',
    'onWindowScrolled'
]);

Window.addListener = function(listener) {
	Window.listeners.add(listener);
}

Window.removeListener = function(listener) {
	Window.listeners.remove(listener);
}

/**
 * Calls when window is resized.
 */
Window.onresize = function() {
	Window.listeners.call('onWindowResized', [Window.getWidth(),
		Window.getHeight()]);
}

/**
 * Calls when window is scrolled.
 */
Window.onscroll = function() {
	Window.listeners.call('onWindowScrolled', [document.body.scrollLeft,
		document.body.scrollTop]);
}

// Register main document
Window.registerDocument(window.document);

// Attach unload, resize and scroll events to be
// handled by Window class commonly.
Event.addListener(window, 'resize', Window.onresize);
Event.addListener(window, 'scroll', Window.onscroll);
Event.addUnloadListener(Window.cleanup);
